LDAP сервер на Erlang за 30 хвилин

Ось може комусь цікаво чому я почав писати SyncML сервер на ерланг. Якщо стисло, мені сподобалося. Якщо довше: я визначаю наскільки просто писати комп'ютерною мовою по тому, наскільки просто прострілити собі ногу (написати LDAP сервер). Завдання таке: написати програму, яка відповідає Windows Mail/Apple Mail, etc. і повертає по LDAP протоколу списки адресатів для автоматичної підстановки (inplace search) в адресних полях листа. Тобто. що б із такого:

дістати ось таке:

Перш за все я зайшов на http://www.rfc-editor.org/rfc/rfc4511.txt і почав завантажувати визначення протоколу LDAP в ASN.1 нотації. Проста копи паста приблизно такого коду:

LDAP DEFINITIONS IMPLICIT TAGS ::= BEGIN LDAPMessage ::= SEQUENCE { messageID MessageID, protocolOp CHOICE { bindRequest BindRequest, bindResponse BindResponse, unbindRequest UnbindRequest, searchRequest SearchRequest, searchResEntry SearchResultEntry, searchResDone SearchResultDone, searchResRef SearchResultReference, modifyRequest ModifyRequest, modifyResponse ModifyResponse, addRequest AddRequest, addResponse AddResponse, delRequest DelRequest, delResponse DelResponse, modDNRequest ModifyDNRequest, modDNResponse ModifyDNResponse, compareRequest CompareRequest, compareResponse CompareResponse, abandonRequest AbandonRequest, extendedReq ExtendedRequest, extendedResp ExtendedResponse, ..., intermediateResponse IntermediateResponse }, controls [0] Controls OPTIONAL } MessageID ::= INTEGER (0 .. maxInt) maxInt INTEGER ::= 2147483647 -- (2^^31 - 1) -- LDAPString ::= OCTET STRING -- UTF-8 encoded, [ISO10646] characters

Далі я за допомогою Ерланг згенерував парсер цієї штуки.

> asn1ct:compile("LDAP.asn1"). ok

Потім я написав код, який відповідає на LDAP запити:

-module(eds). -author('Maxim Sokhatsky '). -export([start/0]). -compile(export_all). -include("LDAP.hrl"). -include("roster.hrl"). -define(TCP_OPTIONS, [binary, {packet, 0}, {active, false}, {reuseaddr, true}]).
start() -> listen(389). listen(Port) -> {ok, LSocket} = gen_tcp:listen(Port, ?TCP_OPTIONS), accept(LSocket). accept(LSocket) -> {ok, Socket} = gen_tcp:accept(LSocket), spawn(fun() -> loop(Socket) end), accept(LSocket). loop(Socket) -> case gen_tcp:recv(Socket, 0) of {ok, Data} -> Decoded = asn1rt:decode('LDAP','LDAPMessage',Data), case Decoded of {ok,{'LDAPMessage',No,Message,Asn}} -> message(No,Message,Socket); _Else -> noLDAP end, loop(Socket); {error, closed} -> ok end.
message(No,Message,Socket) -> io:format("messageID: ~p~n",[No]), io:format("~p~n",[Message]), case Message of {bindRequest, {'BindRequest',Type,Uid,Auth}} -> bind(No,Uid,Auth,Socket); {abandonRequest,Type} -> abandon(No,Socket); {unbindRequest, Null} -> abandon(0,Socket); {searchRequest, {'SearchRequest',SearchDN,Scope,Deref,SizeLimit, TimeLimit,TypesOnly,Filter,Attributes}} -> search(No, SearchDN, Scope,Deref,SizeLimit, TimeLimit,TypesOnly, Filter,Attributes,Socket) end. bind(No,Uid,Auth,Socket) -> roster:init(Uid), Response = #'BindResponse'{resultCode = success, matchedDN = Uid, diagnosticMessage = "OK"}, answer(Response,No,bindResponse,Socket). answer(Response,No,ProtocolOp,Socket) -> Message = #'LDAPMessage'{messageID = No, protocolOp = {ProtocolOp, Response}}, {ok, Bytes} = asn1rt:encode('LDAP', 'LDAPMessage', Message), io:format("~p~n", [Message]), gen_tcp:send(Socket, list_to_binary(Bytes)). search(No,SearchDN,Scope,Deref,SizeLimit, TimeLimit,TypesOnly,Filter,Attributes,Socket) -> roster:traverse(fun({'ContactRecord',CommonName,GivenName,Mail}) -> CN = {'PartialAttribute', "cn", [CommonName]}, MAIL = {'PartialAttribute', "mail", [Mail]}, Response = {'SearchResultEntry', CommonName, [CN,MAIL]}, answer(Response,No,searchResEntry,Socket), continue end), Done = #'LDAPResult'{resultCode = success, matchedDN = SearchDN, diagnosticMessage = "OK"}, answer(Done,No,searchResDone,Socket). abandon(No,Socket) -> roster:done(), gen_tcp:close(Socket).

А також бібліотеку для роботи з контактною книгою:

-module(roster). -include("roster.hrl"). -include_lib("stdlib/include/qlc.hrl"). -compile(export_all). -export([init/1,done/0,put/3,get/1,traverse/1]). init(FileName) -> case ets:info(ram) of undefined -> ets:new(ram, [named_table,{keypos,#'ContactRecord'.cn}, {write_concurrency,true},{read_concurrency,true}]); Else -> ok end, dets:open_file(disk, [{file, FileName},{keypos,#'ContactRecord'.cn}]), ets:from_dets(ram,disk). traverse(Fun) -> dets:traverse(disk,Fun). list() -> ets:foldl(fun(C,Acc) -> io:format("~p~n",[C]) end,none,ram). done() -> case ets:info(ram) of undefined -> ram_empty; ElseClearRam -> ets:delete(ram) end, case dets:info(disk) of {error,Reason} -> disk_closed; ElseCloseDisk -> Res = dets:close(disk), done end. put(CN,GivenName,EMail) -> Contact = #'ContactRecord'{cn = CN, givenName = GivenName, mail = EMail}, ets:insert(ram, Contact), dets:insert(disk, Contact), ok. get(CN) -> case ets:lookup(ram,CN) of [Contact] -> {ok, Contact}; [] -> {error,instance} end.

...У вигляді ерланг-кортежів (рекордів):

-record('ContactRecord', {cn, givenName, mail}).

Відкомпілював це:

$ erlc -o ebin src\LDAP.erl src\eds.erl src\roster.erl

І всьо!

P.S. https://github.com/synrc/ldap